21-6 工厂函数创建不同类型的Ability实例及权限测试
1. Ability工厂函数实现模式
1.1 工厂函数核心逻辑
代码实现详解
/**
* 根据策略数组创建对应的Ability实例集合
* @param policies 权限策略配置数组
* @returns Ability实例数组
*/
function buildAbility(policies: IPolicy[]): Ability[] {
return policies.map(policy => {
switch (policy.type) {
case 0:
return new NormalAbility(policy); // 基础权限控制
case 1:
return new MongoAbility(policy); // MongoDB条件查询
case 2:
return new FunctionalAbility(policy); // 函数式条件判断
default:
throw new Error(`Unsupported policy type: ${policy.type}`);
}
});
}
typescript
设计模式解析
- 工厂模式优势:
- 将对象创建与使用解耦
- 新增Ability类型时只需扩展switch-case分支
- 符合开闭原则(对扩展开放,对修改关闭)
典型应用场景
性能优化建议
- 使用对象映射替代switch-case(当类型较多时):
const abilityConstructors = {
0: NormalAbility,
1: MongoAbility,
2: FunctionalAbility
};
function buildAbility(policies: IPolicy[]) {
return policies.map(policy => {
const Constructor = abilityConstructors[policy.type];
if (!Constructor) throw new Error("Invalid type");
return new Constructor(policy);
});
}
typescript
1.2 策略接口定义
接口规范详解
/**
* 权限策略定义接口
*/
interface IPolicy {
/**
* 策略类型标识
* 0-基础权限 1-MongoDB查询 2-函数式条件
*/
type: number;
/**
* 权限效果
* 'can'-允许访问 'cannot'-拒绝访问
*/
effect: 'can' | 'cannot';
/**
* 操作类型
* 例如:'read'/'write'/'delete'
*/
action: string;
/**
* 资源主体
* 例如:'article'/'user'/'comment'
*/
subject: string;
/**
* 可访问字段白名单
* 仅返回指定的字段(可选)
*/
fields?: string[];
/**
* 动态条件配置
* 可以是:
* - 普通对象 { field: value }
* - MongoDB查询语法 { $gt: ... }
* - 函数表达式 (user) => boolean
*/
conditions?: any;
}
typescript
类型安全增强方案
// 使用泛型约束条件类型
interface IPolicy<T = any> {
// ...其他字段
conditions?: T;
}
// 使用示例
const mongoPolicy: IPolicy<{ $gt: number }> = {
type: 1,
conditions: { $gt: 10 } // 自动类型检查
};
typescript
最佳实践建议
- 字段命名规范:
action
使用动词:create/read/update/delete
subject
使用单数名词:article/user
- 条件复杂度控制:
- 避免超过3层嵌套条件
- 复杂逻辑应拆分为多个策略
扩展思考
💡 如何实现策略版本控制?
- 添加
version
字段 - 使用语义化版本号(如
1.0.0
) - 示例:
interface IPolicy {
// ...原有字段
version?: string;
}
typescript
相关技术对比
方案 | 优点 | 缺点 |
---|---|---|
工厂函数 | 灵活可扩展 | 需手动维护类型映射 |
依赖注入 | 自动装配 | 配置复杂 |
策略模式 | 算法可替换 | 类数量可能爆炸 |
注:可通过
searxng-search
获取最新权限模式设计趋势,了解Casbin等框架的工厂模式实现
2. 三种权限场景测试
2.1 普通场景(type=0)
2.1.1 测试配置详解
// 基础权限策略配置示例
const articleReadPolicy: IPolicy = {
type: 0, // 基础权限类型
effect: 'can',
action: 'read',
subject: 'article',
fields: ['title', 'description', 'createdAt'], // 白名单字段
conditions: {
private: false, // 必须条件
status: 'published' // 附加条件
}
};
typescript
2.1.2 深度测试分析
测试场景 | 请求参数 | 响应结果 | 原理说明 |
---|---|---|---|
公开文章访问 | {private:false, status:'published'} | 返回全部白名单字段 | 完全匹配条件树 |
私有文章请求 | {private:true} | 返回403 Forbidden | 条件短路判断 |
草稿文章请求 | {private:false, status:'draft'} | 返回空数组 | 部分条件不满足 |
字段越权测试 | 请求非白名单字段('content') | 自动过滤该字段 | 字段级权限控制 |
2.1.3 边界测试案例
- 空条件测试:
conditions: {} // 允许所有用户访问
typescript - 多条件组合:
conditions: { $or: [ { private: false }, { ownerId: user.id } ] }
typescript
2.2 MongoDB场景(type=1)
2.2.1 高级查询语法
// 复合查询条件示例
conditions: {
$and: [
{ authorId: { $in: [1, 3, 5] } },
{
$or: [
{ private: false },
{ sharedTo: user.id }
]
},
{ createdAt: { $gt: new Date('2023-01-01') } }
]
}
javascript
2.2.2 性能优化方案
- 索引建议:
// MongoDB集合索引配置 db.articles.createIndex({ authorId: 1, private: 1, createdAt: -1 })
javascript - 查询执行计划分析:
db.articles.find(...).explain("executionStats")
javascript
2.2.3 测试矩阵
2.3 函数式场景(type=2)
2.3.1 高级函数实现
// 支持异步操作的权限函数
conditions: async (user: User) => {
const orgPermissions = await fetchUserOrgPermissions(user.id);
return {
canRead: orgPermissions.includes('content_read'),
canEdit: user.role === 'editor' && article.status !== 'archived'
};
}
typescript
2.3.2 安全传参规范
- 参数消毒方案:
function sanitizeInput(user: unknown): User { // 验证并转换输入参数 }
typescript - 错误处理机制:
try { evaluateAbility([sanitizeInput(user)]); } catch (err) { logError(err); return false; }
typescript
2.3.3 动态策略示例
// 基于环境的条件函数
conditions: (context) => {
if (process.env.NODE_ENV === 'production') {
return { strictCheck: true };
}
return { debugMode: true };
}
javascript
扩展知识:策略组合模式
// 组合多个策略条件
function combinePolicies(policies: IPolicy[]) {
return {
conditions: policies.map(p => p.conditions),
evaluate: (ctx) => policies.every(p => p.evaluate(ctx))
};
}
typescript
测试工具推荐:
- Postman自动化测试集合
- Jest自定义匹配器
- MongoDB Compass查询分析
3. 调试问题解决方案
3.1 参数传递错误
问题分析
在函数式权限校验(type=2
)时,evaluateAbility
方法通常期望接收一个数组形式的参数,但开发者可能会误用展开运算符 ...
,导致运行时错误:
// ❌ 错误示例:展开运算符导致参数不可迭代
evaluateAbility(...user); // TypeError: user is not iterable
typescript
解决方案
- 正确传参方式
// ✅ 正确:显式包装为数组 evaluateAbility([user]);
typescript - 类型安全增强
使用 TypeScript 约束参数类型,避免运行时错误:function evaluateAbility(args: [User]): boolean { // 实现逻辑 }
typescript
调试技巧
- 运行时检查:
if (!Array.isArray(args)) { throw new Error("参数必须是数组"); }
typescript - 日志输出:
console.log("接收参数:", JSON.stringify(args));
typescript
常见误用场景
错误写法 | 正确写法 | 错误原因 |
---|---|---|
evaluateAbility(user) | evaluateAbility([user]) | 缺少数组包装 |
evaluateAbility(...user) | evaluateAbility([user]) | 展开非可迭代对象 |
3.2 变量命名一致性
问题分析
权限系统中的字段命名不一致(如 alaID
vs authorId
)会导致:
- 条件匹配失败:MongoDB 查询或对象属性访问时无法正确识别字段。
- 维护困难:团队协作时需额外记忆不同命名风格。
解决方案
- 强制命名规范
- 使用 ESLint 规则(如
naming-convention
):{ "rules": { "@typescript-eslint/naming-convention": [ "error", { "selector": "property", "format": ["camelCase"], "filter": { "regex": "^(authorId|userId)$", "match": true } } ] } }
json - 代码审查时检查关键字段(如
authorId
、userId
)。
- 使用 ESLint 规则(如
- 动态校验工具
在运行时检查策略对象的字段命名:function validatePolicy(policy: IPolicy) { const reservedFields = ['authorId', 'userId']; reservedFields.forEach(field => { if (policy.conditions && !(field in policy.conditions)) { console.warn(`建议使用标准字段名: ${field}`); } }); }
typescript
修复示例
// ❌ 不一致命名
- conditions: { alaID: 1, post: "123" }
// ✅ 统一命名规范
+ conditions: { authorId: 1, postId: "123" }
diff
命名规范建议
字段类型 | 推荐格式 | 示例 |
---|---|---|
ID 字段 | [entity]Id | authorId 、postId |
布尔字段 | is[状态] | isPrivate |
日期字段 | [动作]At | createdAt |
3.3 边界条件处理(扩展)
典型问题
- 空值处理:
conditions
为null
或undefined
时未容错。 - 类型错误:传递非预期的数据类型(如字符串代替数字)。
防御性编程方案
function safeEvaluate(conditions: unknown, user: User): boolean {
// 1. 空值检查
if (!conditions) return false;
// 2. 类型守卫
if (typeof conditions === 'function') {
return conditions(user);
}
if (typeof conditions === 'object') {
// 3. 动态字段检查
return Object.entries(conditions).every(([key, value]) => {
return user[key] === value;
});
}
throw new Error("无效的 conditions 类型");
}
typescript
测试用例
it("应处理空 conditions", () => {
expect(safeEvaluate(null, user)).toBe(false);
});
it("应校验字段类型", () => {
expect(safeEvaluate({ authorId: "1" }, user)).toBe(false); // 应为 number
});
typescript
总结
问题类型 | 解决方案 | 工具支持 |
---|---|---|
参数传递错误 | 显式数组包装 + 类型约束 | TypeScript + ESLint |
命名不一致 | 强制命名规范 + 动态校验 | ESLint + 代码审查 |
边界条件缺失 | 防御性编程 + 单元测试 | Jest + 类型守卫 |
通过结合静态检查(ESLint)、动态校验和测试覆盖,可系统性减少权限系统的运行时错误。
4. 权限系统演进方向
4.1 数据库存储设计
4.1.1 数据库表结构详解
4.1.2 关键设计考量
- 版本控制方案
- 采用语义化版本(如
1.0.0
) - 变更记录表设计:
CREATE TABLE policy_versions ( version_id SERIAL PRIMARY KEY, policy_id INT REFERENCES policies(policy_id), diff_json JSONB, updated_by INT, created_at TIMESTAMP );
sql
- 采用语义化版本(如
- 条件字段优化
- 按类型分列存储提升查询效率:
ALTER TABLE policies ADD COLUMN mongo_conditions JSONB; ALTER TABLE policies ADD COLUMN functional_conditions TEXT;
sql
- 按类型分列存储提升查询效率:
- 索引策略
CREATE INDEX idx_policy_combinations ON policies(subject, action, type); CREATE INDEX idx_conditions_gin ON policies USING GIN(conditions);
sql
4.1.3 数据关系示例
4.2 Policy Guard集成
4.2.1 增强型守卫实现
@Injectable()
export class PolicyGuard implements CanActivate {
constructor(
private policyService: PolicyService,
private cacheService: CacheService,
private reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const handler = context.getHandler();
// 1. 获取元数据配置
const requiredActions = this.reflector.get<string[]>('actions', handler);
const resourceType = this.reflector.get<string>('resource', handler);
// 2. 缓存策略检查
const cacheKey = `policies:${req.user.id}:${resourceType}`;
let policies = await this.cacheService.get(cacheKey);
if (!policies) {
policies = await this.policyService.getUserPolicies(req.user, resourceType);
await this.cacheService.set(cacheKey, policies, 300); // 5分钟缓存
}
// 3. 动态能力构建
const ability = buildAbility(policies);
// 4. 批量权限校验
return requiredActions.every(action =>
ability.can(action, resourceType)
);
}
}
typescript
4.2.2 元数据装饰器示例
// 控制器级配置
@Resource('article')
@Controller('articles')
export class ArticleController {
@Action(['read'])
@Get(':id')
async getArticle() { /*...*/ }
@Action(['write'])
@Post()
async createArticle() { /*...*/ }
}
typescript
4.2.3 性能优化方案
- 多级缓存策略
- 批量策略预加载
// 启动时预加载高频策略 async preloadPolicies() { const hotResources = ['article', 'user']; await Promise.all( hotResources.map(resource => this.cacheService.warmUp(resource) ) ); }
typescript
4.3 动态策略管理扩展
4.3.1 实时策略更新方案
// WebSocket 监听策略变更
@WebSocketGateway()
export class PolicyUpdateGateway {
@SubscribeMessage('policy-update')
handleUpdate(client: Socket, payload: PolicyUpdateDto) {
this.cacheService.invalidate(`policies:*:${payload.resourceType}`);
client.emit('update-ack', { success: true });
}
}
typescript
4.3.2 策略分析看板
// 策略命中率统计
interface PolicyMetrics {
resourceType: string;
hitRate: number;
avgCheckTime: number;
errorRate: number;
}
typescript
4.3.3 演进路线图
扩展思考
- 多云架构适配:如何设计跨Region的策略同步机制?
- 策略回滚方案:基于Git的版本控制实现
- 性能权衡:强一致性 vs 最终一致性在策略分发中的选择
注:可通过
searxng-search
获取最新策略管理模式,参考AWS IAM或Google Casbin的设计理念
↑